Skip to content

Add cronjob for syncing heartbeats with heartbeat-missing incidents#1954

Open
hmpf wants to merge 6 commits into
mainfrom
heartbeat-checker-cronjob
Open

Add cronjob for syncing heartbeats with heartbeat-missing incidents#1954
hmpf wants to merge 6 commits into
mainfrom
heartbeat-checker-cronjob

Conversation

@hmpf

@hmpf hmpf commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Scope and purpose

Fixes #1947
Depends on #1962

Revieweing tips

The first commit does the heavy lifting, the second adds the actual command.

Contributor Checklist

Every pull request should have this checklist filled out, no matter how small it is.
More information about contributing to Argus can be found in the
Development docs.

  • Added a changelog fragment for towncrier
  • Added/amended tests for new/changed code
  • Added/changed documentation, including updates to the user manual if feature flow or UI is considerably changed
  • Linted/formatted the code with ruff and djLint, easiest by using pre-commit
  • The first line of the commit message continues the sentence "If applied, this commit will ...", starts with a capital letter, does not end with punctuation and is 50 characters or less long. See our how-to
  • If applicable: Created new issues if this PR does not fix the issue completely/there is further work to be done
  • If this results in changes in the UI: Added screenshots of the before and after
  • If this results in changes to the database model: Updated the ER diagram

@hmpf

hmpf commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Oopsies discovered

  • In order to create a details url we need to save the incident twice, to get an incident id. This affects how we make a task for sending notifications. We could just do without any more details, since the info in the incident is all we know anyway.
  • We need to settle on which tags to use, and find a place to document hard-coded tags. See Document magic incidents #1998

@hmpf hmpf moved this from 📋 Backlog to 🏗 In progress in Argus development, public Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Test results

    8 files  1 848 suites   3m 48s ⏱️
1 019 tests 1 018 ✅ 1 💤 0 ❌
8 152 runs  8 144 ✅ 8 💤 0 ❌

Results for commit ae446d0.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.35417% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.04%. Comparing base (c98f817) to head (ae446d0).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...nt/management/commands/sync_heartbeat_incidents.py 86.20% 4 Missing ⚠️
src/argus/incident/heartbeat_utils.py 96.59% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1954      +/-   ##
==========================================
+ Coverage   88.79%   89.04%   +0.24%     
==========================================
  Files         152      154       +2     
  Lines        7276     7466     +190     
==========================================
+ Hits         6461     6648     +187     
- Misses        815      818       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hmpf hmpf mentioned this pull request Jun 18, 2026
8 tasks
@hmpf hmpf changed the title Add rudimentary cronjob for checking heartbeats Add cronjob for syncing heartbeats with heartbeat-missing incidents Jun 22, 2026
@hmpf hmpf changed the base branch from main to duration-field-for-heartbeats June 22, 2026 11:40
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch 3 times, most recently from 9edb660 to 2716a93 Compare June 22, 2026 13:29
@hmpf hmpf mentioned this pull request Jun 23, 2026
4 tasks
@hmpf hmpf force-pushed the duration-field-for-heartbeats branch from 9819f90 to 2526843 Compare June 23, 2026 12:13
Base automatically changed from duration-field-for-heartbeats to main June 23, 2026 12:49
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch from 2716a93 to 210f86e Compare June 23, 2026 12:56
@hmpf hmpf changed the base branch from main to create_fake_incident-refactor June 23, 2026 12:57
@hmpf hmpf force-pushed the create_fake_incident-refactor branch 2 times, most recently from 8ebdaa5 to 39de931 Compare June 24, 2026 09:22
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch from 210f86e to 4fee3d0 Compare June 24, 2026 09:22
@github-actions

github-actions Bot commented Jun 24, 2026

Copy link
Copy Markdown

Warning

2 of 38 new test names are missing convention keywords (given/when/then/it_should)

File Test name
tests/incident/test_heartbeat_utils.py test_close_all_incidents_pointing_to_a_nonexistent_source
tests/incident/test_queryset.py test_from_tags_converts_tagstrings_to_existing_tags_and_returns_a_set_of_tags
Why am I seeing this?

Test names should follow a loose given/when/then pattern with keywords like given, when, then, or it_should. This is a suggestion, not a blocker. The check runs on new test methods added in this PR.

Examples:

  • test_when_no_incidents_then_returns_empty_list
  • test_it_should_create_incident_with_set_description
  • test_given_expired_token_when_refreshing_then_raises_error

@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch from 4fee3d0 to f708f73 Compare June 24, 2026 11:18
@hmpf hmpf force-pushed the create_fake_incident-refactor branch from 9b35864 to fbe1b88 Compare June 25, 2026 13:29
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch 2 times, most recently from a220d49 to 8e9fc88 Compare June 26, 2026 13:23
@hmpf hmpf force-pushed the create_fake_incident-refactor branch from fbe1b88 to 0e6c980 Compare June 26, 2026 13:44
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch from 8e9fc88 to 8c75ae1 Compare June 26, 2026 13:44
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch 2 times, most recently from 0880eb7 to d342583 Compare June 30, 2026 07:54
@hmpf hmpf mentioned this pull request Jun 30, 2026
4 tasks
@hmpf hmpf force-pushed the heartbeat-checker-cronjob branch from d342583 to 27163f8 Compare July 1, 2026 06:57

@stveit stveit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posting this as "comment" since one comment is a nitpick and the other might be a "request change", but im not entirely sure about the intentions of the code

Comment thread src/argus/incident/heartbeat_utils.py Outdated
Comment thread src/argus/incident/heartbeat_utils.py Outdated
Comment thread docs/reference/special-incidents.rst Outdated
Comment thread src/argus/incident/heartbeat_utils.py Outdated
Comment thread src/argus/incident/heartbeat_utils.py Outdated
Comment thread src/argus/incident/models.py
Comment thread src/argus/incident/heartbeat_utils.py Outdated
Comment thread src/argus/incident/models.py Outdated
Comment thread tests/incident/management_commands/test_sync_heartbeat_incidents.py Outdated
@johannaengland johannaengland self-requested a review July 2, 2026 09:45
Comment on lines 108 to +109
def TestCreateStatelessIncident(TestCase):
def test_create_stateless_incident(self):
def test_it_should_always_return_a_stateless_incident(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test class was b0rked before this PR ....

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A.K.A. Always start with empty tests, run them and observe they are failing (so you know they have been discovered by the test runner) - then implement them.

def _close_incidents_whose_sources_are_alive_again(timestamp):
argus = SourceSystem.objects.get(name="argus")
still_dead_sources = SourceSystem.objects.dead(timestamp - _FUDGE)
outdated_incidents = Incident.objects.heartbeat_incidents().open()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this rather open_heartbeat_incidents - because we don't know if they are outdated yet

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call what? Which of the four lines?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outdated_incidents -> open_heartbeat_incidents



def _create_incidents_for_dead_sources(timestamp: Optional[datetime] = None):
# Create existing incidents whose sources have become dead

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment confuses me - are you creating them or are they existing? And this should be a docstring

timestamp: Optional[datetime] = None,
level: int = DEFAULT_LEVEL,
):
assert isinstance(source, SourceSystem), "source is not a SourceSystem"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert isinstance(source, SourceSystem), "source is not a SourceSystem"
assert isinstance(source, SourceSystem), f"source {source} is not a SourceSystem"

Comment thread src/argus/incident/heartbeat_utils.py Outdated
return qs

def dead(self, timestamp: Optional[datetime] = None):
"""Find sources that have missed heartbeats

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be helpful to mention in the docstring what the timestamp means in this context

self.assertFalse(result)

def test_when_incident_has_both_magical_heartbeat_tags_return_True(self):
source_name = "hearteat-source"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
source_name = "hearteat-source"
source_name = "heartbeat-source"

self.assertNotIn(tag3, result)
self.assertEqual(set((tag1, tag2)), result)

def test_given_existing_tags_from_tag_keys_returns_a_queryset_of_tags_the_given_tag_key(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_given_existing_tags_from_tag_keys_returns_a_queryset_of_tags_the_given_tag_key(self):
def test_given_existing_tags_from_tag_keys_returns_a_queryset_of_tags_with_the_given_tag_key(self):


@tag("db")
class TestIncidentQuerySetTagMethods(TestCase):
def test_when_no_incident_has_source_with_key_return_empty_queryset(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source?

Comment thread changelog.d/1954.added.md
@@ -0,0 +1,4 @@
Added management command suitable to run as a cronjob that looks for dead
incidents (as per heartbeat freqeuncy and last seen heartbeat). Dead sources

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
incidents (as per heartbeat freqeuncy and last seen heartbeat). Dead sources
incidents (as per heartbeat frequency and last seen heartbeat). Dead sources

Comment thread docs/reference/special-incidents.rst Outdated
@github-project-automation github-project-automation Bot moved this from ❓ Ready for review to ♻ Changes requested in Argus development, public Jul 2, 2026
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@hmpf hmpf requested a review from johannaengland July 2, 2026 14:21
@hmpf hmpf moved this from ♻ Changes requested to ❓ Ready for review in Argus development, public Jul 2, 2026
DEFAULT_LEVEL = 4
_FUDGE = timedelta(seconds=DEFAULT_FUDGE_FACTOR)
HEARTBEAT_FUDGE = timedelta(seconds=DEFAULT_FUDGE_FACTOR)
INCIDENT_DESCRIPTION_TEMPLATE = "Missing heartbeat from source {source}, dead?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
INCIDENT_DESCRIPTION_TEMPLATE = "Missing heartbeat from source {source}, dead?"
INCIDENT_DESCRIPTION_TEMPLATE = "Missing heartbeat from source {source} ({type}), dead?"

def _close_heartbeat_incidents(timestamp: datetime) -> Tuple[List[SourceSystem], List[Incident], List[Incident]]:
"""Close all existing heartbeat incidents that can be closed

Returns a list of reanimated sources foll: owed by a list of freshly

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Returns a list of reanimated sources foll: owed by a list of freshly
Returns a list of reanimated sources followed by a list of freshly

"""Close all existing heartbeat incidents that can be closed

Returns a list of reanimated sources foll: owed by a list of freshly
created incidents and ifinally a list of incidents that could not be closed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
created incidents and ifinally a list of incidents that could not be closed.
created incidents and finally a list of incidents that could not be closed.

return sources, closed_incidents, remaining_incidents


def _attempt_closing_heartbeat_incident(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great idea to factor this out!

freshly created (True) or not (False).

Logs if there are multiple incidents for the source, and returns one of the
sources and that it was not feshly created (False).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sources and that it was not feshly created (False).
incidents and that it was not freshly created (False).

# @transaction.atomic
def set_closed(self, actor: User, timestamp: datetime = None, description=""):
"Incident closed by user"
"Incident closed by human user"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Incident closed by human user"
"Closes incident as a human user"
``

# @transaction.atomic
def set_end(self, actor: User, timestamp: datetime = None, description: str = ""):
"Incident closed by source"
"Incident ended by machine source"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Incident ended by machine source"
"Ends incident as a machine source"

@johannaengland johannaengland requested review from a team and stveit July 3, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: ❓ Ready for review

Development

Successfully merging this pull request may close these issues.

Make a cronjob for discovering dead sources

4 participants